/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.harmony.luni.util;

import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * Utility functions for IPV6 operations.
 */
public class Inet6Util {

	static String hexCharacters = "0123456789ABCDEF";

	public static String addressToString(int value) {
		return ((value >> 24) & 0xff) + "." + ((value >> 16) & 0xff) + "."
				+ ((value >> 8) & 0xff) + "." + (value & 0xff);
	}

	/**
	 * Takes the byte array and creates an integer out of four bytes starting at
	 * start as the high-order byte. This method makes no checks on the validity
	 * of the parameters.
	 */
	public static int bytesToInt(byte bytes[], int start) {
		// First mask the byte with 255, as when a negative
		// signed byte converts to an integer, it has bits
		// on in the first 3 bytes, we are only concerned
		// about the right-most 8 bits.
		// Then shift the rightmost byte to align with its
		// position in the integer.
		final int value = ((bytes[start + 3] & 255))
				| ((bytes[start + 2] & 255) << 8)
				| ((bytes[start + 1] & 255) << 16)
				| ((bytes[start] & 255) << 24);
		return value;
	}

	/** Converts a 4 character hex word into a 2 byte word equivalent */
	public static void convertToBytes(String hexWord, byte ipByteArray[],
			int byteIndex) {

		final int hexWordLength = hexWord.length();
		int hexWordIndex = 0;
		ipByteArray[byteIndex] = 0;
		ipByteArray[byteIndex + 1] = 0;
		int charValue;

		// high order 4 bits of first byte
		if (hexWordLength > 3) {
			charValue = getIntValue(hexWord.charAt(hexWordIndex++));
			ipByteArray[byteIndex] = (byte) (ipByteArray[byteIndex] | (charValue << 4));
		}

		// low order 4 bits of the first byte
		if (hexWordLength > 2) {
			charValue = getIntValue(hexWord.charAt(hexWordIndex++));
			ipByteArray[byteIndex] = (byte) (ipByteArray[byteIndex] | charValue);
		}

		// high order 4 bits of second byte
		if (hexWordLength > 1) {
			charValue = getIntValue(hexWord.charAt(hexWordIndex++));
			ipByteArray[byteIndex + 1] = (byte) (ipByteArray[byteIndex + 1] | (charValue << 4));
		}

		// low order 4 bits of the first byte
		charValue = getIntValue(hexWord.charAt(hexWordIndex));
		ipByteArray[byteIndex + 1] = (byte) (ipByteArray[byteIndex + 1] | charValue & 15);
	}

	/**
	 * Creates an byte[] based on an ipAddressString. No error handling is
	 * performed here.
	 */
	public static byte[] createByteArrayFromIPAddressString(
			String ipAddressString) {

		if (isValidIPV4Address(ipAddressString)) {
			final StringTokenizer tokenizer = new StringTokenizer(
					ipAddressString, ".");
			String token = "";
			int tempInt = 0;
			final byte[] byteAddress = new byte[4];
			for (int i = 0; i < 4; i++) {
				token = tokenizer.nextToken();
				tempInt = Integer.parseInt(token);
				byteAddress[i] = (byte) tempInt;
			}

			return byteAddress;
		}

		if (ipAddressString.charAt(0) == '[') {
			ipAddressString = ipAddressString.substring(1,
					ipAddressString.length() - 1);
		}

		final StringTokenizer tokenizer = new StringTokenizer(ipAddressString,
				":.", true);
		final ArrayList<String> hexStrings = new ArrayList<String>();
		final ArrayList<String> decStrings = new ArrayList<String>();
		String token = "";
		String prevToken = "";
		// If a double colon exists, we need to insert 0s.
		int doubleColonIndex = -1;

		/*
		 * Go through the tokens, including the separators ':' and '.' When we
		 * hit a : or . the previous token will be added to either the hex list
		 * or decimal list. In the case where we hit a :: we will save the index
		 * of the hexStrings so we can add zeros in to fill out the string
		 */
		while (tokenizer.hasMoreTokens()) {
			prevToken = token;
			token = tokenizer.nextToken();

			if (token.equals(":")) {
				if (prevToken.equals(":")) {
					doubleColonIndex = hexStrings.size();
				} else if (!prevToken.equals("")) {
					hexStrings.add(prevToken);
				}
			} else if (token.equals(".")) {
				decStrings.add(prevToken);
			}
		}

		if (prevToken.equals(":")) {
			if (token.equals(":")) {
				doubleColonIndex = hexStrings.size();
			} else {
				hexStrings.add(token);
			}
		} else if (prevToken.equals(".")) {
			decStrings.add(token);
		}

		// figure out how many hexStrings we should have
		// also check if it is a IPv4 address
		int hexStringsLength = 8;

		// If we have an IPv4 address tagged on at the end, subtract
		// 4 bytes, or 2 hex words from the total
		if (decStrings.size() > 0) {
			hexStringsLength -= 2;
		}

		// if we hit a double Colon add the appropriate hex strings
		if (doubleColonIndex != -1) {
			final int numberToInsert = hexStringsLength - hexStrings.size();
			for (int i = 0; i < numberToInsert; i++) {
				hexStrings.add(doubleColonIndex, "0");
			}
		}

		final byte ipByteArray[] = new byte[16];

		// Finally convert these strings to bytes...
		for (int i = 0; i < hexStrings.size(); i++) {
			convertToBytes(hexStrings.get(i), ipByteArray, i * 2);
		}

		// Now if there are any decimal values, we know where they go...
		for (int i = 0; i < decStrings.size(); i++) {
			ipByteArray[i + 12] = (byte) (Integer.parseInt(decStrings.get(i)) & 255);
		}

		// now check to see if this guy is actually and IPv4 address
		// an ipV4 address is ::FFFF:d.d.d.d
		boolean ipV4 = true;
		for (int i = 0; i < 10; i++) {
			if (ipByteArray[i] != 0) {
				ipV4 = false;
				break;
			}
		}

		if (ipByteArray[10] != -1 || ipByteArray[11] != -1) {
			ipV4 = false;
		}

		if (ipV4) {
			final byte ipv4ByteArray[] = new byte[4];
			for (int i = 0; i < 4; i++) {
				ipv4ByteArray[i] = ipByteArray[i + 12];
			}
			return ipv4ByteArray;
		}

		return ipByteArray;

	}

	public static String createIPAddrStringFromByteArray(byte ipByteArray[]) {
		if (ipByteArray.length == 4) {
			return addressToString(bytesToInt(ipByteArray, 0));
		}

		if (ipByteArray.length == 16) {
			if (isIPv4MappedAddress(ipByteArray)) {
				final byte ipv4ByteArray[] = new byte[4];
				for (int i = 0; i < 4; i++) {
					ipv4ByteArray[i] = ipByteArray[i + 12];
				}
				return addressToString(bytesToInt(ipv4ByteArray, 0));
			}
			final StringBuilder buffer = new StringBuilder();
			boolean isFirst = true;
			for (int i = 0; i < ipByteArray.length; i++) {
				if ((i & 1) == 0) {
					isFirst = true;
				}
				int j = (ipByteArray[i] & 0xf0) >>> 4;
				if (j != 0 || !isFirst) {
					buffer.append(hexCharacters.charAt(j));
					isFirst = false;
				}
				j = ipByteArray[i] & 0x0f;
				if (j != 0 || !isFirst) {
					buffer.append(hexCharacters.charAt(j));
					isFirst = false;
				}
				if ((i & 1) != 0 && (i + 1) < ipByteArray.length) {
					if (isFirst) {
						buffer.append('0');
					}
					buffer.append(':');
				}
			}
			return buffer.toString();
		}
		return null;
	}

	static int getIntValue(char c) {

		switch (c) {
		case '0':
			return 0;
		case '1':
			return 1;
		case '2':
			return 2;
		case '3':
			return 3;
		case '4':
			return 4;
		case '5':
			return 5;
		case '6':
			return 6;
		case '7':
			return 7;
		case '8':
			return 8;
		case '9':
			return 9;
		}

		c = Character.toLowerCase(c);
		switch (c) {
		case 'a':
			return 10;
		case 'b':
			return 11;
		case 'c':
			return 12;
		case 'd':
			return 13;
		case 'e':
			return 14;
		case 'f':
			return 15;
		}
		return 0;
	}

	public static boolean isIP6AddressInFullForm(String ipAddress) {
		if (isValidIP6Address(ipAddress)) {
			final int doubleColonIndex = ipAddress.indexOf("::");
			if (doubleColonIndex >= 0) {
				// Simplified form which contains ::
				return false;
			}
			return true;
		}
		return false;
	}

	private static boolean isIPv4MappedAddress(byte ipAddress[]) {

		// Check if the address matches ::FFFF:d.d.d.d
		// The first 10 bytes are 0. The next to are -1 (FF).
		// The last 4 bytes are varied.
		for (int i = 0; i < 10; i++) {
			if (ipAddress[i] != 0) {
				return false;
			}
		}

		if (ipAddress[10] != -1 || ipAddress[11] != -1) {
			return false;
		}

		return true;

	}

	static boolean isValidHexChar(char c) {

		return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')
				|| (c >= 'a' && c <= 'f');
	}

	public static boolean isValidIP4Word(String word) {
		char c;
		if (word.length() < 1 || word.length() > 3) {
			return false;
		}
		for (int i = 0; i < word.length(); i++) {
			c = word.charAt(i);
			if (!(c >= '0' && c <= '9')) {
				return false;
			}
		}
		if (Integer.parseInt(word) > 255) {
			return false;
		}
		return true;
	}

	public static boolean isValidIP6Address(String ipAddress) {
		final int length = ipAddress.length();
		boolean doubleColon = false;
		int numberOfColons = 0;
		int numberOfPeriods = 0;
		int numberOfPercent = 0;
		String word = "";
		char c = 0;
		char prevChar = 0;
		int offset = 0; // offset for [] IP addresses

		if (length < 2) {
			return false;
		}

		for (int i = 0; i < length; i++) {
			prevChar = c;
			c = ipAddress.charAt(i);
			switch (c) {

			// case for an open bracket [x:x:x:...x]
			case '[':
				if (i != 0) {
					return false; // must be first character
				}
				if (ipAddress.charAt(length - 1) != ']') {
					return false; // must have a close ]
				}
				offset = 1;
				if (length < 4) {
					return false;
				}
				break;

			// case for a closed bracket at end of IP [x:x:x:...x]
			case ']':
				if (i != length - 1) {
					return false; // must be last character
				}
				if (ipAddress.charAt(0) != '[') {
					return false; // must have a open [
				}
				break;

			// case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d
			case '.':
				numberOfPeriods++;
				if (numberOfPeriods > 3) {
					return false;
				}
				if (!isValidIP4Word(word)) {
					return false;
				}
				if (numberOfColons != 6 && !doubleColon) {
					return false;
				}
				// a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an
				// IPv4 ending, otherwise 7 :'s is bad
				if (numberOfColons == 7 && ipAddress.charAt(0 + offset) != ':'
						&& ipAddress.charAt(1 + offset) != ':') {
					return false;
				}
				word = "";
				break;

			case ':':
				numberOfColons++;
				if (numberOfColons > 7) {
					return false;
				}
				if (numberOfPeriods > 0) {
					return false;
				}
				if (prevChar == ':') {
					if (doubleColon) {
						return false;
					}
					doubleColon = true;
				}
				word = "";
				break;
			case '%':
				if (numberOfColons == 0) {
					return false;
				}
				numberOfPercent++;

				// validate that the stuff after the % is valid
				if ((i + 1) >= length) {
					// in this case the percent is there but no number is
					// available
					return false;
				}
				try {
					Integer.parseInt(ipAddress.substring(i + 1));
				} catch (final NumberFormatException e) {
					// right now we just support an integer after the % so if
					// this is not
					// what is there then return
					return false;
				}
				break;

			default:
				if (numberOfPercent == 0) {
					if (word.length() > 3) {
						return false;
					}
					if (!isValidHexChar(c)) {
						return false;
					}
				}
				word += c;
			}
		}

		// Check if we have an IPv4 ending
		if (numberOfPeriods > 0) {
			if (numberOfPeriods != 3 || !isValidIP4Word(word)) {
				return false;
			}
		} else {
			// If we're at then end and we haven't had 7 colons then there is a
			// problem unless we encountered a doubleColon
			if (numberOfColons != 7 && !doubleColon) {
				return false;
			}

			// If we have an empty word at the end, it means we ended in either
			// a : or a .
			// If we did not end in :: then this is invalid
			if (numberOfPercent == 0) {
				if (word == "" && ipAddress.charAt(length - 1 - offset) == ':'
						&& ipAddress.charAt(length - 2 - offset) != ':') {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Takes a string and parses it to see if it is a valid IPV4 address.
	 * 
	 * @return true, if the string represents an IPV4 address in dotted
	 *         notation, false otherwise
	 */
	public static boolean isValidIPV4Address(String value) {
		// general test
		if (!value.matches("[\\p{Digit}\\.]*")) {
			return false;
		}

		final String[] parts = value.split("\\.");
		final int length = parts.length;
		if (length > 4) {
			return false;
		}

		// for one part
		if (parts.length == 1) {
			final long longValue = Long.parseLong(parts[0]);
			return longValue >= 0 && longValue <= 0xFFFFFFFFL;
		}
		// test every parts
		for (final String part : parts) {
			if (part.length() > 3 || Integer.parseInt(part) > 255) {
				return false;
			}
		}
		return true;
	}

}
